// Copyright (c) 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gfx/color_transform.h"

#include <vector>

#include "base/logging.h"
#include "third_party/qcms/src/qcms.h"
#include "ui/gfx/color_space.h"
#include "ui/gfx/icc_profile.h"
#include "ui/gfx/transform.h"

#ifndef THIS_MUST_BE_INCLUDED_AFTER_QCMS_H
extern "C" {
#include "third_party/qcms/src/chain.h"
};
#endif

namespace gfx {

Transform Invert(const Transform& t)
{
    Transform ret = t;
    if (!t.GetInverse(&ret)) {
        LOG(ERROR) << "Inverse should alsways be possible.";
    }
    return ret;
}

ColorTransform::TriStim Map(const Transform& t, ColorTransform::TriStim color)
{
    t.TransformPoint(&color);
    return color;
}

ColorTransform::TriStim Xy2xyz(float x, float y)
{
    return ColorTransform::TriStim(x, y, 1.0f - x - y);
}

void GetPrimaries(ColorSpace::PrimaryID id,
    ColorTransform::TriStim primaries[4])
{
    switch (id) {
    case ColorSpace::PrimaryID::CUSTOM:
        NOTREACHED();

    case ColorSpace::PrimaryID::RESERVED0:
    case ColorSpace::PrimaryID::RESERVED:
    case ColorSpace::PrimaryID::UNSPECIFIED:
    case ColorSpace::PrimaryID::UNKNOWN:
    case ColorSpace::PrimaryID::BT709:
        // BT709 is our default case. Put it after the switch just
        // in case we somehow get an id which is not listed in the switch.
        // (We don't want to use "default", because we want the compiler
        //  to tell us if we forgot some enum values.)
        break;

    case ColorSpace::PrimaryID::BT470M:
        // Red
        primaries[0] = Xy2xyz(0.67f, 0.33f);
        // Green
        primaries[1] = Xy2xyz(0.21f, 0.71f);
        // Blue
        primaries[2] = Xy2xyz(0.14f, 0.08f);
        // Whitepoint
        primaries[3] = Xy2xyz(0.31f, 0.316f);
        return;

    case ColorSpace::PrimaryID::BT470BG:
        // Red
        primaries[0] = Xy2xyz(0.64f, 0.33f);
        // Green
        primaries[1] = Xy2xyz(0.29f, 0.60f);
        // Blue
        primaries[2] = Xy2xyz(0.15f, 0.06f);
        // Whitepoint (D65f)
        primaries[3] = Xy2xyz(0.3127f, 0.3290f);
        return;

    case ColorSpace::PrimaryID::SMPTE170M:
    case ColorSpace::PrimaryID::SMPTE240M:
        // Red
        primaries[0] = Xy2xyz(0.630f, 0.340f);
        // Green
        primaries[1] = Xy2xyz(0.310f, 0.595f);
        // Blue
        primaries[2] = Xy2xyz(0.155f, 0.070f);
        // Whitepoint (D65f)
        primaries[3] = Xy2xyz(0.3127f, 0.3290f);
        return;

    case ColorSpace::PrimaryID::FILM:
        // Red
        primaries[0] = Xy2xyz(0.681f, 0.319f);
        // Green
        primaries[1] = Xy2xyz(0.243f, 0.692f);
        // Blue
        primaries[2] = Xy2xyz(0.145f, 0.049f);
        // Whitepoint (Cf)
        primaries[3] = Xy2xyz(0.310f, 0.136f);
        return;

    case ColorSpace::PrimaryID::BT2020:
        // Red
        primaries[0] = Xy2xyz(0.708f, 0.292f);
        // Green
        primaries[1] = Xy2xyz(0.170f, 0.797f);
        // Blue
        primaries[2] = Xy2xyz(0.131f, 0.046f);
        // Whitepoint (D65f)
        primaries[3] = Xy2xyz(0.3127f, 0.3290f);
        return;

    case ColorSpace::PrimaryID::SMPTEST428_1:
        // X
        primaries[0] = Xy2xyz(1.0f, 0.0f);
        // Y
        primaries[1] = Xy2xyz(0.0f, 1.0f);
        // Z
        primaries[2] = Xy2xyz(0.0f, 0.0f);
        // Whitepoint (Ef)
        primaries[3] = Xy2xyz(1.0f / 3.0f, 1.0f / 3.0f);
        return;

    case ColorSpace::PrimaryID::SMPTEST431_2:
        // Red
        primaries[0] = Xy2xyz(0.680f, 0.320f);
        // Green
        primaries[1] = Xy2xyz(0.265f, 0.690f);
        // Blue
        primaries[2] = Xy2xyz(0.150f, 0.060f);
        // Whitepoint
        primaries[3] = Xy2xyz(0.314f, 0.351f);
        return;

    case ColorSpace::PrimaryID::SMPTEST432_1:
        // Red
        primaries[0] = Xy2xyz(0.680f, 0.320f);
        // Green
        primaries[1] = Xy2xyz(0.265f, 0.690f);
        // Blue
        primaries[2] = Xy2xyz(0.150f, 0.060f);
        // Whitepoint (D65f)
        primaries[3] = Xy2xyz(0.3127f, 0.3290f);
        return;

    case ColorSpace::PrimaryID::XYZ_D50:
        // X
        primaries[0] = Xy2xyz(1.0f, 0.0f);
        // Y
        primaries[1] = Xy2xyz(0.0f, 1.0f);
        // Z
        primaries[2] = Xy2xyz(0.0f, 0.0f);
        // D50
        primaries[3] = Xy2xyz(0.34567f, 0.35850f);
        return;
    }

    // Red
    primaries[0] = Xy2xyz(0.640f, 0.330f);
    // Green
    primaries[1] = Xy2xyz(0.300f, 0.600f);
    // Blue
    primaries[2] = Xy2xyz(0.150f, 0.060f);
    // Whitepoint (D65f)
    primaries[3] = Xy2xyz(0.3127f, 0.3290f);
}

GFX_EXPORT Transform GetPrimaryMatrix(ColorSpace::PrimaryID id)
{
    ColorTransform::TriStim primaries[4];
    GetPrimaries(id, primaries);
    ColorTransform::TriStim WXYZ(primaries[3].x() / primaries[3].y(), 1.0f,
        primaries[3].z() / primaries[3].y());

    Transform ret(
        primaries[0].x(), primaries[1].x(), primaries[2].x(), 0.0f, // 1
        primaries[0].y(), primaries[1].y(), primaries[2].y(), 0.0f, // 2
        primaries[0].z(), primaries[1].z(), primaries[2].z(), 0.0f, // 3
        0.0f, 0.0f, 0.0f, 1.0f); // 4

    ColorTransform::TriStim conv = Map(Invert(ret), WXYZ);
    ret.Scale3d(conv.x(), conv.y(), conv.z());

    // Chromatic adaptation.
    Transform bradford(0.8951000f, 0.2664000f, -0.1614000f, 0.0f, // 1
        -0.7502000f, 1.7135000f, 0.0367000f, 0.0f, // 2
        0.0389000f, -0.0685000f, 1.0296000f, 0.0f, // 3
        0.0f, 0.0f, 0.0f, 1.0f); // 4

    ColorTransform::TriStim D50(0.9642f, 1.0f, 0.8249f);
    ColorTransform::TriStim source_response = Map(bradford, WXYZ);
    ColorTransform::TriStim dest_response = Map(bradford, D50);

    Transform adapter;
    adapter.Scale3d(dest_response.x() / source_response.x(),
        dest_response.y() / source_response.y(),
        dest_response.z() / source_response.z());

    return Invert(bradford) * adapter * bradford * ret;
}

GFX_EXPORT float FromLinear(ColorSpace::TransferID id, float v)
{
    switch (id) {
    case ColorSpace::TransferID::SMPTEST2084_NON_HDR:
        // Should already be handled.
        NOTREACHED();
    case ColorSpace::TransferID::CUSTOM:
    // TODO(hubbe): Actually implement custom transfer functions.
    case ColorSpace::TransferID::RESERVED0:
    case ColorSpace::TransferID::RESERVED:
    case ColorSpace::TransferID::UNSPECIFIED:
    case ColorSpace::TransferID::UNKNOWN:
        // All unknown values default to BT709

    case ColorSpace::TransferID::BT709:
    case ColorSpace::TransferID::SMPTE170M:
    case ColorSpace::TransferID::BT2020_10:
    case ColorSpace::TransferID::BT2020_12:
        // BT709 is our "default" cause, so put the code after the switch
        // to avoid "control reaches end of non-void function" errors.
        break;

    case ColorSpace::TransferID::GAMMA22:
        v = fmax(0.0f, v);
        return powf(v, 1.0f / 2.2f);

    case ColorSpace::TransferID::GAMMA28:
        v = fmax(0.0f, v);
        return powf(v, 1.0f / 2.8f);

    case ColorSpace::TransferID::SMPTE240M: {
        v = fmax(0.0f, v);
        float a = 1.11157219592173128753f;
        float b = 0.02282158552944503135f;
        if (v <= b) {
            return 4.0f * v;
        } else {
            return a * powf(v, 0.45f) - (a - 1.0f);
        }
    }

    case ColorSpace::TransferID::LINEAR:
        return v;

    case ColorSpace::TransferID::LOG:
        if (v < 0.01f)
            return 0.0f;
        return 1.0f + log(v) / log(10.0f) / 2.0f;

    case ColorSpace::TransferID::LOG_SQRT:
        if (v < sqrt(10.0f) / 1000.0f)
            return 0.0f;
        return 1.0f + log(v) / log(10.0f) / 2.5f;

    case ColorSpace::TransferID::IEC61966_2_4: {
        float a = 1.099296826809442f;
        float b = 0.018053968510807f;
        if (v < -b) {
            return -a * powf(-v, 0.45f) + (a - 1.0f);
        } else if (v <= b) {
            return 4.5f * v;
        } else {
            return a * powf(v, 0.45f) - (a - 1.0f);
        }
    }

    case ColorSpace::TransferID::BT1361_ECG: {
        float a = 1.099f;
        float b = 0.018f;
        float l = 0.0045f;
        if (v < -l) {
            return -(a * powf(-4.0f * v, 0.45f) + (a - 1.0f)) / 4.0f;
        } else if (v <= b) {
            return 4.5f * v;
        } else {
            return a * powf(v, 0.45f) - (a - 1.0f);
        }
    }

    case ColorSpace::TransferID::IEC61966_2_1: { // SRGB
        v = fmax(0.0f, v);
        float a = 1.055f;
        float b = 0.0031308f;
        if (v < b) {
            return 12.92f * v;
        } else {
            return a * powf(v, 1.0f / 2.4f) - (a - 1.0f);
        }
    }
    case ColorSpace::TransferID::SMPTEST2084: {
        v = fmax(0.0f, v);
        float m1 = (2610.0f / 4096.0f) / 4.0f;
        float m2 = (2523.0f / 4096.0f) * 128.0f;
        float c1 = 3424.0f / 4096.0f;
        float c2 = (2413.0f / 4096.0f) * 32.0f;
        float c3 = (2392.0f / 4096.0f) * 32.0f;
        return powf((c1 + c2 * powf(v, m1)) / (1.0f + c3 * powf(v, m1)), m2);
    }

    case ColorSpace::TransferID::SMPTEST428_1:
        v = fmax(0.0f, v);
        return powf(48.0f * v + 52.37f, 1.0f / 2.6f);

    // Spec: http://www.arib.or.jp/english/html/overview/doc/2-STD-B67v1_0.pdf
    case ColorSpace::TransferID::ARIB_STD_B67: {
        const float a = 0.17883277f;
        const float b = 0.28466892f;
        const float c = 0.55991073f;
        const float Lmax = 12.0f;
        v = Lmax * fmax(0.0f, v);
        if (v <= 1)
            return 0.5f * sqrtf(v);
        else
            return a * log(v - b) + c;
    }

    // Chrome-specific values below
    case ColorSpace::TransferID::GAMMA24:
        v = fmax(0.0f, v);
        return powf(v, 1.0f / 2.4f);
    }

    v = fmax(0.0f, v);
    float a = 1.099296826809442f;
    float b = 0.018053968510807f;
    if (v <= b) {
        return 4.5f * v;
    } else {
        return a * powf(v, 0.45f) - (a - 1.0f);
    }
}

GFX_EXPORT float ToLinear(ColorSpace::TransferID id, float v)
{
    switch (id) {
    case ColorSpace::TransferID::CUSTOM:
    // TODO(hubbe): Actually implement custom transfer functions.
    case ColorSpace::TransferID::RESERVED0:
    case ColorSpace::TransferID::RESERVED:
    case ColorSpace::TransferID::UNSPECIFIED:
    case ColorSpace::TransferID::UNKNOWN:
        // All unknown values default to BT709

    case ColorSpace::TransferID::BT709:
    case ColorSpace::TransferID::SMPTE170M:
    case ColorSpace::TransferID::BT2020_10:
    case ColorSpace::TransferID::BT2020_12:
        // BT709 is our "default" cause, so put the code after the switch
        // to avoid "control reaches end of non-void function" errors.
        break;

    case ColorSpace::TransferID::GAMMA22:
        v = fmax(0.0f, v);
        return powf(v, 2.2f);

    case ColorSpace::TransferID::GAMMA28:
        v = fmax(0.0f, v);
        return powf(v, 2.8f);

    case ColorSpace::TransferID::SMPTE240M: {
        v = fmax(0.0f, v);
        float a = 1.11157219592173128753f;
        float b = 0.02282158552944503135f;
        if (v <= FromLinear(ColorSpace::TransferID::SMPTE240M, b)) {
            return v / 4.0f;
        } else {
            return powf((v + a - 1.0f) / a, 1.0f / 0.45f);
        }
    }

    case ColorSpace::TransferID::LINEAR:
        return v;

    case ColorSpace::TransferID::LOG:
        if (v < 0.0f)
            return 0.0f;
        return powf(10.0f, (v - 1.0f) * 2.0f);

    case ColorSpace::TransferID::LOG_SQRT:
        if (v < 0.0f)
            return 0.0f;
        return powf(10.0f, (v - 1.0f) * 2.5f);

    case ColorSpace::TransferID::IEC61966_2_4: {
        float a = 1.099296826809442f;
        float b = 0.018053968510807f;
        if (v < FromLinear(ColorSpace::TransferID::IEC61966_2_4, -a)) {
            return -powf((a - 1.0f - v) / a, 1.0f / 0.45f);
        } else if (v <= FromLinear(ColorSpace::TransferID::IEC61966_2_4, b)) {
            return v / 4.5f;
        } else {
            return powf((v + a - 1.0f) / a, 1.0f / 0.45f);
        }
    }

    case ColorSpace::TransferID::BT1361_ECG: {
        float a = 1.099f;
        float b = 0.018f;
        float l = 0.0045f;
        if (v < FromLinear(ColorSpace::TransferID::BT1361_ECG, -l)) {
            return -powf((1.0f - a - v * 4.0f) / a, 1.0f / 0.45f) / 4.0f;
        } else if (v <= FromLinear(ColorSpace::TransferID::BT1361_ECG, b)) {
            return v / 4.5f;
        } else {
            return powf((v + a - 1.0f) / a, 1.0f / 0.45f);
        }
    }

    case ColorSpace::TransferID::IEC61966_2_1: { // SRGB
        v = fmax(0.0f, v);
        float a = 1.055f;
        float b = 0.0031308f;
        if (v < FromLinear(ColorSpace::TransferID::IEC61966_2_1, b)) {
            return v / 12.92f;
        } else {
            return powf((v + a - 1.0f) / a, 2.4f);
        }
    }

    case ColorSpace::TransferID::SMPTEST2084: {
        v = fmax(0.0f, v);
        float m1 = (2610.0f / 4096.0f) / 4.0f;
        float m2 = (2523.0f / 4096.0f) * 128.0f;
        float c1 = 3424.0f / 4096.0f;
        float c2 = (2413.0f / 4096.0f) * 32.0f;
        float c3 = (2392.0f / 4096.0f) * 32.0f;
        return powf(
            fmax(powf(v, 1.0f / m2) - c1, 0) / (c2 - c3 * powf(v, 1.0f / m2)),
            1.0f / m1);
    }

    case ColorSpace::TransferID::SMPTEST428_1:
        return (powf(v, 2.6f) - 52.37f) / 48.0f;

    // Chrome-specific values below
    case ColorSpace::TransferID::GAMMA24:
        v = fmax(0.0f, v);
        return powf(v, 2.4f);

    case ColorSpace::TransferID::SMPTEST2084_NON_HDR:
        v = fmax(0.0f, v);
        return fmin(2.3f * pow(v, 2.8f), v / 5.0f + 0.8f);

    // Spec: http://www.arib.or.jp/english/html/overview/doc/2-STD-B67v1_0.pdf
    case ColorSpace::TransferID::ARIB_STD_B67: {
        v = fmax(0.0f, v);
        const float a = 0.17883277f;
        const float b = 0.28466892f;
        const float c = 0.55991073f;
        const float Lmax = 12.0f;
        float v_ = 0.0f;
        if (v <= 0.5f) {
            v_ = (v * 2.0f) * (v * 2.0f);
        } else {
            v_ = exp((v - c) / a) + b;
        }
        return v_ / Lmax;
    }
    }

    v = fmax(0.0f, v);
    float a = 1.099296826809442f;
    float b = 0.018053968510807f;
    if (v < FromLinear(ColorSpace::TransferID::BT709, b)) {
        return v / 4.5f;
    } else {
        return powf((v + a - 1.0f) / a, 1.0f / 0.45f);
    }
}

namespace {
    // Assumes bt2020
    float Luma(const ColorTransform::TriStim& c)
    {
        return c.x() * 0.2627f + c.y() * 0.6780f + c.z() * 0.0593f;
    }
};

GFX_EXPORT ColorTransform::TriStim ToLinear(ColorSpace::TransferID id,
    ColorTransform::TriStim color)
{
    ColorTransform::TriStim ret(ToLinear(id, color.x()), ToLinear(id, color.y()),
        ToLinear(id, color.z()));

    if (id == ColorSpace::TransferID::SMPTEST2084_NON_HDR) {
        if (Luma(ret) > 0.0) {
            ColorTransform::TriStim smpte2084(
                ToLinear(ColorSpace::TransferID::SMPTEST2084, color.x()),
                ToLinear(ColorSpace::TransferID::SMPTEST2084, color.y()),
                ToLinear(ColorSpace::TransferID::SMPTEST2084, color.z()));
            smpte2084.Scale(Luma(ret) / Luma(smpte2084));
            ret = smpte2084;
        }
    }

    return ret;
}

GFX_EXPORT Transform GetTransferMatrix(ColorSpace::MatrixID id)
{
    // Default values for BT709;
    float Kr = 0.2126f;
    float Kb = 0.0722f;
    switch (id) {
    case ColorSpace::MatrixID::RGB:
        return Transform();

    case ColorSpace::MatrixID::BT709:
    case ColorSpace::MatrixID::UNSPECIFIED:
    case ColorSpace::MatrixID::RESERVED:
    case ColorSpace::MatrixID::UNKNOWN:
        // Default values are already set.
        break;

    case ColorSpace::MatrixID::FCC:
        Kr = 0.30f;
        Kb = 0.11f;
        break;

    case ColorSpace::MatrixID::BT470BG:
    case ColorSpace::MatrixID::SMPTE170M:
        Kr = 0.299f;
        Kb = 0.144f;
        break;

    case ColorSpace::MatrixID::SMPTE240M:
        Kr = 0.212f;
        Kb = 0.087f;
        break;

    case ColorSpace::MatrixID::YCOCG:
        return Transform(0.25f, 0.5f, 0.25f, 0.5f, // 1
            -0.25f, 0.5f, -0.25f, 0.5f, // 2
            0.5f, 0.0f, -0.5f, 0.0f, // 3
            0.0f, 0.0f, 0.0f, 1.0f); // 4

    // TODO(hubbe): Check if the CL equation is right.
    case ColorSpace::MatrixID::BT2020_NCL:
    case ColorSpace::MatrixID::BT2020_CL:
        Kr = 0.2627f;
        Kb = 0.0593f;
        break;

    case ColorSpace::MatrixID::YDZDX:
        return Transform(0.0f, 1.0f, 0.0f, 0.0f, // 1
            0.0f, -0.5f, 0.986566f / 2.0f, 0.5f, // 2
            0.5f, -0.991902f / 2.0f, 0.0f, 0.5f, // 3
            0.0f, 0.0f, 0.0f, 1.0f); // 4
    }
    float u_m = 0.5f / (1.0f - Kb);
    float v_m = 0.5f / (1.0f - Kr);
    return Transform(
        Kr, 1.0f - Kr - Kb, Kb, 0.0f, // 1
        u_m * -Kr, u_m * -(1.0f - Kr - Kb), u_m * (1.0f - Kb), 0.5f, // 2
        v_m * (1.0f - Kr), v_m * -(1.0f - Kr - Kb), v_m * -Kb, 0.5f, // 3
        0.0f, 0.0f, 0.0f, 1.0f); // 4
}

Transform GetRangeAdjustMatrix(ColorSpace::RangeID range,
    ColorSpace::MatrixID matrix)
{
    switch (range) {
    case ColorSpace::RangeID::FULL:
    case ColorSpace::RangeID::UNSPECIFIED:
        return Transform();

    case ColorSpace::RangeID::DERIVED:
    case ColorSpace::RangeID::LIMITED:
        break;
    }
    switch (matrix) {
    case ColorSpace::MatrixID::RGB:
    case ColorSpace::MatrixID::YCOCG:
        return Transform(255.0f / 219.0f, 0.0f, 0.0f, -16.0f / 219.0f, // 1
            0.0f, 255.0f / 219.0f, 0.0f, -16.0f / 219.0f, // 2
            0.0f, 0.0f, 255.0f / 219.0f, -16.0f / 219.0f, // 3
            0.0f, 0.0f, 0.0f, 1.0f); // 4

    case ColorSpace::MatrixID::BT709:
    case ColorSpace::MatrixID::UNSPECIFIED:
    case ColorSpace::MatrixID::RESERVED:
    case ColorSpace::MatrixID::FCC:
    case ColorSpace::MatrixID::BT470BG:
    case ColorSpace::MatrixID::SMPTE170M:
    case ColorSpace::MatrixID::SMPTE240M:
    case ColorSpace::MatrixID::BT2020_NCL:
    case ColorSpace::MatrixID::BT2020_CL:
    case ColorSpace::MatrixID::YDZDX:
    case ColorSpace::MatrixID::UNKNOWN:
        return Transform(255.0f / 219.0f, 0.0f, 0.0f, -16.0f / 219.0f, // 1
            0.0f, 255.0f / 224.0f, 0.0f, -15.5f / 224.0f, // 2
            0.0f, 0.0f, 255.0f / 224.0f, -15.5f / 224.0f, // 3
            0.0f, 0.0f, 0.0f, 1.0f); // 4
    }
    NOTREACHED();
    return Transform();
}

class ColorSpaceToColorSpaceTransform : public ColorTransform {
public:
    ColorSpaceToColorSpaceTransform(const ColorSpace& from,
        const ColorSpace& to,
        Intent intent)
        : from_(from)
        , to_(to)
    {
        if (intent == Intent::INTENT_PERCEPTUAL) {
            switch (from_.transfer_) {
            case ColorSpace::TransferID::UNSPECIFIED:
            case ColorSpace::TransferID::BT709:
            case ColorSpace::TransferID::SMPTE170M:
                // See SMPTE 1886
                from_.transfer_ = ColorSpace::TransferID::GAMMA24;
                break;

            case ColorSpace::TransferID::SMPTEST2084:
                // We don't have an HDR display, so replace SMPTE 2084 with something
                // that returns ranges more or less suitable for a normal display.
                from_.transfer_ = ColorSpace::TransferID::SMPTEST2084_NON_HDR;
                break;

            case ColorSpace::TransferID::ARIB_STD_B67:
                // Interpreting HLG using a gamma 2.4 works reasonably well for SDR
                // displays. Once we have HDR output capabilies, we'll need to
                // change this.
                from_.transfer_ = ColorSpace::TransferID::GAMMA24;
                break;

            default: // Do nothing
                break;
            }

            // TODO(hubbe): shrink gamuts here (never stretch gamuts)
        }

        Transform* from_transfer_matrix = from_.matrix_ == ColorSpace::MatrixID::BT2020_CL ? &b_ : &a_;
        Transform* to_transfer_matrix = to_.matrix_ == ColorSpace::MatrixID::BT2020_CL ? &b_ : &c_;

        c_ *= Invert(GetRangeAdjustMatrix(to_.range_, to_.matrix_));
        *to_transfer_matrix *= GetTransferMatrix(to_.matrix_);
        b_ *= Invert(GetPrimaryTransform(to_));
        b_ *= GetPrimaryTransform(from_);
        *from_transfer_matrix *= Invert(GetTransferMatrix(from_.matrix_));
        a_ *= GetRangeAdjustMatrix(from_.range_, from_.matrix_);
    }

    static Transform GetPrimaryTransform(const ColorSpace& c)
    {
        if (c.primaries_ == ColorSpace::PrimaryID::CUSTOM) {
            return Transform(c.custom_primary_matrix_[0], c.custom_primary_matrix_[1],
                c.custom_primary_matrix_[2], c.custom_primary_matrix_[3],
                c.custom_primary_matrix_[4], c.custom_primary_matrix_[5],
                c.custom_primary_matrix_[6], c.custom_primary_matrix_[7],
                c.custom_primary_matrix_[8], c.custom_primary_matrix_[9],
                c.custom_primary_matrix_[10],
                c.custom_primary_matrix_[11], 0.0f, 0.0f, 0.0f, 1.0f);
        } else {
            return GetPrimaryMatrix(c.primaries_);
        }
    }

    void transform(TriStim* colors, size_t num) override
    {
        for (size_t i = 0; i < num; i++) {
            TriStim c = colors[i];
            a_.TransformPoint(&c);
            c.set_x(ToLinear(from_.transfer_, c.x()));
            c.set_y(ToLinear(from_.transfer_, c.y()));
            c.set_z(ToLinear(from_.transfer_, c.z()));
            b_.TransformPoint(&c);
            c.set_x(FromLinear(to_.transfer_, c.x()));
            c.set_y(FromLinear(to_.transfer_, c.y()));
            c.set_z(FromLinear(to_.transfer_, c.z()));
            c_.TransformPoint(&c);
            colors[i] = c;
        }
    }

private:
    ColorSpace from_;
    ColorSpace to_;

    // a_ -> tolinear -> b_ -> fromlinear -> c_;
    Transform a_;
    Transform b_;
    Transform c_;
};

class QCMSColorTransform : public ColorTransform {
public:
    // Takes ownership of the profiles
    QCMSColorTransform(qcms_profile* from, qcms_profile* to)
        : from_(from)
        , to_(to)
    {
    }
    ~QCMSColorTransform() override
    {
        qcms_profile_release(from_);
        qcms_profile_release(to_);
    }
    void transform(TriStim* colors, size_t num) override
    {
        CHECK(sizeof(TriStim) == sizeof(float[3]));
        // QCMS doesn't like numbers outside 0..1
        for (size_t i = 0; i < num; i++) {
            colors[i].set_x(fmin(1.0f, fmax(0.0f, colors[i].x())));
            colors[i].set_y(fmin(1.0f, fmax(0.0f, colors[i].y())));
            colors[i].set_z(fmin(1.0f, fmax(0.0f, colors[i].z())));
        }
        qcms_chain_transform(from_, to_, reinterpret_cast<float*>(colors),
            reinterpret_cast<float*>(colors), num * 3);
    }

private:
    qcms_profile *from_, *to_;
};

class ChainColorTransform : public ColorTransform {
public:
    ChainColorTransform(std::unique_ptr<ColorTransform> a,
        std::unique_ptr<ColorTransform> b)
        : a_(std::move(a))
        , b_(std::move(b))
    {
    }

private:
    void transform(TriStim* colors, size_t num) override
    {
        a_->transform(colors, num);
        b_->transform(colors, num);
    }
    std::unique_ptr<ColorTransform> a_;
    std::unique_ptr<ColorTransform> b_;
};

qcms_profile* GetQCMSProfileIfAvailable(const ColorSpace& color_space)
{
    ICCProfile icc_profile = ICCProfile::FromColorSpace(color_space);
    if (icc_profile.GetData().empty())
        return nullptr;
    return qcms_profile_from_memory(icc_profile.GetData().data(),
        icc_profile.GetData().size());
}

qcms_profile* GetXYZD50Profile()
{
    // QCMS is trixy, it has a datatype called qcms_CIE_xyY, but what it expects
    // is in fact not xyY color coordinates, it just wants the x/y values of the
    // primaries with Y equal to 1.0.
    qcms_CIE_xyYTRIPLE xyz;
    qcms_CIE_xyY w;
    xyz.red.x = 1.0f;
    xyz.red.y = 0.0f;
    xyz.red.Y = 1.0f;
    xyz.green.x = 0.0f;
    xyz.green.y = 1.0f;
    xyz.green.Y = 1.0f;
    xyz.blue.x = 0.0f;
    xyz.blue.y = 0.0f;
    xyz.blue.Y = 1.0f;
    w.x = 0.34567f;
    w.y = 0.35850f;
    w.Y = 1.0f;
    return qcms_profile_create_rgb_with_gamma(w, xyz, 1.0f);
}

std::unique_ptr<ColorTransform> ColorTransform::NewColorTransform(
    const ColorSpace& from,
    const ColorSpace& to,
    Intent intent)
{
    qcms_profile* from_profile = GetQCMSProfileIfAvailable(from);
    qcms_profile* to_profile = GetQCMSProfileIfAvailable(to);
    if (from_profile) {
        if (to_profile) {
            return std::unique_ptr<ColorTransform>(
                new QCMSColorTransform(from_profile, to_profile));
        } else {
            return std::unique_ptr<ColorTransform>(new ChainColorTransform(
                std::unique_ptr<ColorTransform>(
                    new QCMSColorTransform(from_profile, GetXYZD50Profile())),
                std::unique_ptr<ColorTransform>(new ColorSpaceToColorSpaceTransform(
                    ColorSpace::CreateXYZD50(), to, intent))));
        }
    } else {
        if (to_profile) {
            return std::unique_ptr<ColorTransform>(new ChainColorTransform(
                std::unique_ptr<ColorTransform>(new ColorSpaceToColorSpaceTransform(
                    from, ColorSpace::CreateXYZD50(), intent)),
                std::unique_ptr<ColorTransform>(
                    new QCMSColorTransform(GetXYZD50Profile(), to_profile))));
        } else {
            return std::unique_ptr<ColorTransform>(
                new ColorSpaceToColorSpaceTransform(from, to, intent));
        }
    }
}

} // namespace gfx
